package uk.co.whichdigital.ksv

import java.time.LocalDate
import java.time.LocalDateTime
import kotlin.reflect.*

private const val CSV_DEFAULT_NAME: String = "CSV_VALUE_DEFAULT_NAME"

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CsvRow

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CsvValue(
    val name: String = CSV_DEFAULT_NAME
)

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CsvTimestamp(
    val name: String = CSV_DEFAULT_NAME,
    /**format is either a single timestamp pattern (e.g. "yyyy/MM/dd" ) or multiple separated by '|' (e.g. "yyyy/MM/dd|dd-MM-yyyy" )*/
    val format: String
)

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CsvGeneric(
    val name: String = CSV_DEFAULT_NAME,
    val converterName: String
)

sealed class CsvRowParam(
    val rowClassName: String,
    val param: KParameter,
    val normalizedColumnName: String
) {
    /** has a default value */
    val isParamOptional: Boolean get() = param.isOptional
    val isParamNullable: Boolean get() = paramType.isMarkedNullable
    val paramType: KType get() = param.type
    val paramName: String get() = param.name!!

    class ByNoAnnotation(
        rowClassName: String,
        param: KParameter,
        normalizeName: String.() -> String
    ) : CsvRowParam(
        rowClassName,
        param,
        param.name!!.normalizeName()
    )

    class ByCsvValue(
        rowClassName: String,
        param: KParameter,
        csvValue: CsvValue,
        normalizeName: String.() -> String
    ) : CsvRowParam(
        rowClassName,
        param,
        pickName(csvValue.name, param.name).normalizeName()
    )

    class ByCsvTimestamp(
        rowClassName: String,
        param: KParameter,
        csvTimestamp: CsvTimestamp,
        normalizeName: String.() -> String
    ) : CsvRowParam(
        rowClassName,
        param,
        pickName(csvTimestamp.name, param.name).normalizeName()
    ) {
        val format: String = csvTimestamp.format

        init {
            val paramClass = paramType.classifier as KClass<*>
            if (!supportedTimestampTypes.contains(paramClass)) {
                throw IllegalArgumentException(
                    "$rowClassName.$paramName is of type class ${paramClass.simpleName} which is currently not supported as target for @CsvTimestamp. " +
                            "Please use one of these classes: ${supportedTimestampTypes.map { it.simpleName }.joinToString()}"
                )
            }
        }

        companion object {
            val supportedTimestampTypes: Set<KClass<*>> = setOf(LocalDate::class, LocalDateTime::class)
        }
    }

    class ByCsvGeneric(
        rowClassName: String,
        param: KParameter,
        csvGeneric: CsvGeneric,
        normalizeName: String.() -> String
    ) : CsvRowParam(
        rowClassName,
        param,
        pickName(csvGeneric.name, param.name).normalizeName()
    ) {
        val converterName: String = csvGeneric.converterName

        init {
            val converter: GenericConverter<*> = GenericConverterRegistry[converterName]
            val paramClass: KClass<*> = paramType.classifier as KClass<*>
            if (!converter.isAssignableTo(paramClass)) {
                throw IllegalArgumentException(
                    """
                    The converter ${converter.name} can't be used for parameter $paramName.
                    The value generated by this converter is not assignable to Parameter class ${paramClass.qualifiedName}.
                    """.trimIndent()
                )
            }
        }
    }

    companion object {
        private fun pickName(annotatedName: String, defaultParamName: String?): String {
            return if (annotatedName != CSV_DEFAULT_NAME) {
                annotatedName
            } else {
                defaultParamName!!
            }
        }
    }
}

private inline fun <reified T> KAnnotatedElement.getSingleAnnotation(): T? =
    this.annotations.filterIsInstance<T>().singleOrNull()

private inline fun <reified T> KAnnotatedElement.hasAnnotation(): Boolean =
    this.annotations.filterIsInstance<T>().isNotEmpty()

class ReflectiveItemFactory<Row : Any>(
    rowClass: KClass<Row>,
    private val normalizeParamName: String.() -> String = { this }
) {

    private val rowClassName: String
    private val cvsRowConstructor: KFunction<Row>
    private val csvRowParamByNormalizedColumnName: Map<String, CsvRowParam>

    init {
        if (!rowClass.hasAnnotation<CsvRow>()) {
            throw IllegalArgumentException("The parameter class must be annotated with @CsvRow but ${rowClass.qualifiedName} isn't.")
        }
        rowClassName = rowClass.qualifiedName!!
        cvsRowConstructor = rowClass.constructors.singleOrNull()
            ?: throw IllegalArgumentException("The CsvRow class $rowClassName mustn't have more than one constructor!")

        val constructorParams: List<KParameter> = cvsRowConstructor.parameters
        val csvRowParamsByNormalizedColumnName: Map<String, List<CsvRowParam>> = constructorParams.map { param ->
            val csvValue: CsvValue? = param.getSingleAnnotation<CsvValue>()
            val csvTimestamp: CsvTimestamp? = param.getSingleAnnotation<CsvTimestamp>()
            val csvGeneric: CsvGeneric? = param.getSingleAnnotation<CsvGeneric>()
            return@map when {
                csvValue != null -> CsvRowParam.ByCsvValue(
                    rowClassName,
                    param,
                    csvValue,
                    normalizeParamName
                )
                csvTimestamp != null -> CsvRowParam.ByCsvTimestamp(
                    rowClassName,
                    param,
                    csvTimestamp,
                    normalizeParamName
                )
                csvGeneric != null -> CsvRowParam.ByCsvGeneric(
                    rowClassName,
                    param,
                    csvGeneric,
                    normalizeParamName
                )
                else -> CsvRowParam.ByNoAnnotation(
                    rowClassName,
                    param,
                    normalizeParamName
                )
            }
        }.groupBy { it.normalizedColumnName }

        csvRowParamsByNormalizedColumnName[""].let {affectedParameterList->
            if(affectedParameterList!=null) {
                val affectedParameterNames: String = affectedParameterList.joinToString(prefix = "[", postfix = "]")
                throw IllegalStateException(
                    "Class ${rowClass.simpleName} contains @CsvX-annotated parameter with empty name. Affected parameter: $affectedParameterNames"
                )
            }
        }

        csvRowParamByNormalizedColumnName = if (constructorParams.size == csvRowParamsByNormalizedColumnName.size) {
            csvRowParamsByNormalizedColumnName.entries.map { (normalizeParamName, csvRowParams) ->
                normalizeParamName to csvRowParams.single()
            }.toMap()
        } else {
            val paramNamesByNormalizedParamName: Map<String, List<String>> =
                csvRowParamsByNormalizedColumnName.entries.filter { (_, csvRowParams) ->
                    csvRowParams.size > 1
                }.map { (normalizeParamName, csvRowParams) ->
                    normalizeParamName to csvRowParams.map { csvRowParam -> csvRowParam.paramName }
                }.toMap()
            val problematicNormalizedParamNames: String =
                paramNamesByNormalizedParamName.keys.joinToString(prefix = "[", postfix = "]")
            val affectedParameterNames: String =
                paramNamesByNormalizedParamName.values.flatten().joinToString(prefix = "[", postfix = "]")
            throw IllegalStateException(
                """
                
                Not every constructor parameter in class ${rowClass.simpleName} got a unique mapped name (probably due to normalization)!
                problematic normalizedNames: $problematicNormalizedParamNames,
                affected parameters: $affectedParameterNames
                """.trimIndent()
            )
        }
    }

    fun getSuperfluousNormalizedColumnNames(header: CsvHeader): Set<String> {

        fun <T> Iterable<T>.countOccurence(): Map<T, Int> = this.groupingBy { it }.eachCount()
        fun <T> List<T>.elementsWithMultipleOccurence(): Set<Pair<T, Int>> =
            this.countOccurence().entries.filter { it.value > 1 }.map { it.key to it.value }.toSet()

        fun <T> List<T>.assertNoMultipleOccurence(listName: String): Set<T> = this.let { list ->
            val set = list.toSet()
            if (set.size == list.size) {
                return set
            } else {
                val elementsWithMultipleOccurence: Set<Pair<T, Int>> = list.elementsWithMultipleOccurence()
                throw IllegalArgumentException(
                    """
                    some columnNames show up multiple times in $listName: 
                    ${elementsWithMultipleOccurence.joinToString { "${it.first} (${it.second}times)" }}
                    """.trimIndent()
                )
            }
        }

        val providedNames: Set<String> = header.normalizedColumnNames.assertNoMultipleOccurence("(csv-)header")
        val neededNames: Set<String> = csvRowParamByNormalizedColumnName.keys

        if (!providedNames.containsAll(neededNames)) {
            val missingNames = neededNames.filter { !providedNames.contains(it) }
            throw IllegalArgumentException("csv table lacks the following (normalized) parameter: ${missingNames.joinToString()}")
        }

        return if (providedNames.size == neededNames.size) {
            emptySet()
        } else {
            providedNames.filter { !neededNames.contains(it) }.toSet()
        }
    }

    fun buildItem(actualNamesWithToken: Map<String, String?>): Row {

        fun normalizeKeys(actualNamesWithToken: Map<String, String?>): Map<String, String?> {
            // normalize parameter names
            val normalizedNamesWithValue: Map<String, String?> = actualNamesWithToken.entries.map { (name, token) ->
                name.normalizeParamName() to token
            }.toMap()
            if (actualNamesWithToken.size != normalizedNamesWithValue.size) {
                throw IllegalArgumentException(
                    """
                Normalization has erased the distinction between two names!
                actual param names: ${actualNamesWithToken.keys},
                normalized param names: ${normalizedNamesWithValue.keys}                ,
                """.trimIndent()
                )
            }

            return normalizedNamesWithValue
        }

        val normalizedNamesWithToken: Map<String, String?> = normalizeKeys(actualNamesWithToken)

        // map the [name and String value] to [kparameter and concrete Any? value]
        val nonNullableParamNamesWithNullValue = mutableSetOf<String>()
        val paramsWithValue: Map<KParameter, Any?> =
            normalizedNamesWithToken.entries.mapNotNull { (normalizedName, token) ->
                val csvRowParam: CsvRowParam = csvRowParamByNormalizedColumnName[normalizedName]
                    ?: throw IllegalStateException("didn't find a CsvRowParam for normalizedName: $normalizedName")
                val value: Any? = convert(token, csvRowParam)

                if (value == null) {
                    if (csvRowParam.isParamOptional) {
                        // removed null value so that default value is selected on callBy-invocation
                        return@mapNotNull null
                    }
                    if (!csvRowParam.isParamNullable) {
                        nonNullableParamNamesWithNullValue.add(csvRowParam.normalizedColumnName)
                        return@mapNotNull null
                    }
                }

                csvRowParam.param to value
            }.toMap()

        if (nonNullableParamNamesWithNullValue.isNotEmpty()) {
            throw IllegalArgumentException(
                "found ${nonNullableParamNamesWithNullValue.size} non-nullable parameter(s) with null value: ${
                nonNullableParamNamesWithNullValue.joinToString()
                }"
            )
        }

        return cvsRowConstructor.callBy(paramsWithValue)
    }
}

fun <ItemType : Any> record2Item(
    header: CsvHeader,
    record: CsvRecord,
    itemFactory: ReflectiveItemFactory<ItemType>
): Record2ItemResult<ItemType> {
    val providedNormalizedColumnNames = header.normalizedColumnNames
    val unusedNormalizedColumnNames: Set<String> = itemFactory.getSuperfluousNormalizedColumnNames(header)

    val actualParamNamesWithValue: Map<String, String?> =
        providedNormalizedColumnNames.mapIndexedNotNull { index, columnName ->
            if (unusedNormalizedColumnNames.contains(columnName)) {
                return@mapIndexedNotNull null
            }
            columnName to record.getAsNonBlankStringOrNull(index)
        }.toMap()

    return try {
        Record2ItemResult.Success(
            itemFactory.buildItem(
                actualParamNamesWithValue
            )
        )
    } catch (e: Exception) {
        Record2ItemResult.ConversionException(e)
    }
}

sealed class Record2ItemResult<out T : Any> {
    class Success<out T : Any>(val item: T) : Record2ItemResult<T>()
    class ConversionException(val e: Exception) : Record2ItemResult<Nothing>()
}
